using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Runtime.Serialization;

using SharpGL.Media3D;
using SharpGL.SceneGraph.Cameras;
using SharpGL.SceneGraph.Effects;
using SharpGL.SceneGraph.Assets;
using SharpGL.SceneGraph.Transformations;

namespace SharpGL.SceneGraph
{
	[TypeConverter(typeof(System.ComponentModel.ExpandableObjectConverter))]
    [DataContract(/*IsReference = true, */Name = "Scene", Namespace = "SharpGL.SceneGraph")]
    public class Scene : IHasOpenGLContext, INotifyContextChanged, INotifyPropertyChanged, INotifySceneTreeChanged
    {
        #region Private Consts

        private static readonly OpenGL defaultCurrentOpenGLContext = null;
        private static readonly GLColor defaultClearColour = new GLColor(0, 0, 0, 0);
        private static readonly Camera defaultCamera = new OrthographicCamera();
        private static readonly bool defaultAutoScaleToUnitVolume = false;

        #endregion Private Consts

        #region Private Fields

        /// <summary>
        /// This is the OpenGL class, use it to call OpenGL functions.
        /// </summary>
        [IgnoreDataMember()]
        private OpenGL currentOpenGLContext;

        /// <summary>
        /// The main scene container - this is the top level element of the Scene Tree.
        /// </summary>
        [DataMember()]
        private SceneContainer sceneContainer;

        /// <summary>
        /// The set of scene assets.
        /// </summary>
        [DataMember()]
        private ObservableCollection<Asset> assets;

        /// <summary>
        /// This is the colour of the background of the scene.
        /// </summary>
        [DataMember()]
        private GLColor clearColour;

        /// <summary>
        /// This is the camera that is currently being used to view the scene.
        /// </summary>
        [DataMember()]
        private Camera camera;

        /// <summary>
        ///  A value indicating whether auto scale to unit volume is enabled.
        /// </summary>
        [DataMember()]
        private bool autoScaleToUnitVolume;

        /// <summary>
        /// Transformation used for scale a scene to unit volume.
        /// </summary>
        [IgnoreDataMember()]
        private LinearTransformation transformationToUnitVolume;

        #endregion Private Fields

        #region Private Methods

        private void InitializeTransfomationToUnitVolume(LinearTransformation transformation, BoundingVolume sceneBoundingVolume)
        {
            if (transformation == null)
                throw new ArgumentNullException("transformation");

            if (sceneBoundingVolume == null)
                throw new ArgumentNullException("sceneBoundingVolume");

            Size3D dim;
            float maxDim;
            float sceneScaleFactor;

            if (sceneBoundingVolume.IsEmpty)
                dim = new Size3D(0.0f, 0.0f, 0.0f);
            else
                dim = sceneBoundingVolume.Size;

            maxDim = dim.X;
            if (maxDim < dim.Y)
                maxDim = dim.Y;
            if (maxDim < dim.Z)
                maxDim = dim.Z;

            sceneScaleFactor = (maxDim == 0.0f ? 1.0f : 1.0f / maxDim);

            transformation.ScaleX = sceneScaleFactor;
            transformation.ScaleY = sceneScaleFactor;
            transformation.ScaleZ = sceneScaleFactor;

            transformation.TranslationVector = (Vector3D)sceneBoundingVolume.Centre * -sceneScaleFactor;
        }
        
        private bool SearchEffect(SceneElement root, SceneElement stopElement, SceneElement excludedElement, Effect effect)
        {
            if (root != stopElement)
            {
                if (root != excludedElement)
                    foreach (Effect rootEffect in root.Effects)
                        if (rootEffect == effect)
                            return true;

                foreach (SceneElement child in root.Children)
                    if (SearchEffect(child, stopElement, excludedElement, effect))
                        return true;
            }

            return false;
        }

        private void CreateInContext(OpenGL gl, SceneElement sceneElement)
        {
            //  Create scene element.
            if (sceneElement is IHasOpenGLContext &&
                !OpenGL.IsValidContext(((IHasOpenGLContext)sceneElement).CurrentOpenGLContext))
                ((IHasOpenGLContext)sceneElement).CreateInContext(gl);

            //  Create all effects of scene element.
            foreach (Effect effect in sceneElement.Effects)
                if (effect is IHasOpenGLContext &&
                    !OpenGL.IsValidContext(((IHasOpenGLContext)effect).CurrentOpenGLContext))
                    ((IHasOpenGLContext)effect).CreateInContext(gl);

            //  Create all children.
            foreach (SceneElement childElement in sceneElement.Children)
                CreateInContext(gl, childElement);
        }

        private void DestroyInContext(OpenGL gl, SceneElement sceneElement)
        {
            //  Destroy all children.
            foreach (SceneElement childElement in sceneElement.Children)
                DestroyInContext(gl, childElement);

            //  Destroy all effects of a scene element (if them don't exist in other part of scene tree).
            foreach (Effect effect in sceneElement.Effects)
                if (effect is IHasOpenGLContext &&
                    OpenGL.IsValidContext(((IHasOpenGLContext)effect).CurrentOpenGLContext) &&
                    !SearchEffect(sceneElement.TraverseToRootElement(), sceneElement, sceneElement, effect))
                    ((IHasOpenGLContext)effect).DestroyInContext();

            //  Destroy scene a element.
            if (sceneElement is IHasOpenGLContext &&
                OpenGL.IsValidContext(((IHasOpenGLContext)sceneElement).CurrentOpenGLContext))
                ((IHasOpenGLContext)sceneElement).DestroyInContext();
        }

        private void OnContextChanged(object sender, ContextChangeEventArgs e)
        {
            if (autoScaleToUnitVolume)
                InitializeTransfomationToUnitVolume(transformationToUnitVolume, sceneContainer.ChildrenBoundingVolume);

            NotifyContextChanged(sender, e);
        }

        [OnDeserialized]
        private void OnDeserialized(StreamingContext ctx)
        {
            OnDeserialized();
        }

        [OnDeserializing]
        private void OnDeserializing(StreamingContext ctx)
        {
            OnDeserializing();
        }

        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (autoScaleToUnitVolume && !(sender is Camera))
                InitializeTransfomationToUnitVolume(transformationToUnitVolume, sceneContainer.ChildrenBoundingVolume);

            NotifyPropertyChanged(sender.GetType() + "." + e.PropertyName);
        }

        /// <summary>
        /// Called when scene tree changed.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">A SceneTreeChangeEventArgs that contains the event data.</param>
        private void OnSceneTreeChanged(object sender, SceneTreeChangeEventArgs e)
        {
            if (OpenGL.IsValidContext(currentOpenGLContext))
            {
                switch (e.Action)
                {
                    case SceneTreeChangeAction.AddElement:
                        CreateInContext(currentOpenGLContext, e.SceneElement);

                        break;

                    case SceneTreeChangeAction.RemoveElement:
                        DestroyInContext(currentOpenGLContext, e.SceneElement);

                        break;

                    case SceneTreeChangeAction.AddEffect:
                        if (e.Effect is IHasOpenGLContext &&
                            !OpenGL.IsValidContext(((IHasOpenGLContext)e.Effect).CurrentOpenGLContext))
                            ((IHasOpenGLContext)e.Effect).CreateInContext(currentOpenGLContext);

                        break;

                    case SceneTreeChangeAction.RemoveEffect:
                        if (e.Effect is IHasOpenGLContext &&
                            OpenGL.IsValidContext(((IHasOpenGLContext)e.Effect).CurrentOpenGLContext) &&
                            !SearchEffect(sceneContainer, null, (SceneElement)sender, e.Effect))
                            ((IHasOpenGLContext)e.Effect).DestroyInContext();

                        break;
                }
            }

            if (autoScaleToUnitVolume)
                InitializeTransfomationToUnitVolume(transformationToUnitVolume, sceneContainer.ChildrenBoundingVolume);

            NotifySceneTreeChanged(sender, e);
        }

        #endregion Private Methods

        #region Protected Methods

        /// <summary>
        /// Call this function to do the SceneTreeChanged event.
        /// </summary>
        protected void NotifySceneTreeChanged(object sender, SceneTreeChangeEventArgs e)
        {
            var handler = SceneTreeChanged;
            if (handler != null)
                handler(sender, e);
        }

        /// <summary>
        /// Call this function to do the ContextChanged event.
        /// </summary>
        protected void NotifyContextChanged(object sender, ContextChangeEventArgs e)
        {
            var handler = ContextChanged;
            if (handler != null)
                handler(sender, e);
        }

        /// <summary>
        /// Call this function to do the PropertyChanged event.
        /// </summary>
        protected void NotifyPropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }

        protected virtual void OnDeserialized()
        {
            if (autoScaleToUnitVolume)
                InitializeTransfomationToUnitVolume(transformationToUnitVolume, sceneContainer.ChildrenBoundingVolume);
            else
                transformationToUnitVolume.Reset();
        }

        protected virtual void OnDeserializing()
        {
            currentOpenGLContext = defaultCurrentOpenGLContext;
            transformationToUnitVolume = new LinearTransformation();
        }

        /// <summary>
        /// Renders the element for hit test.
        /// </summary>
        /// <param name="gl">The OpenGL context.</param>
        /// <param name="sceneElement">The scene element.</param>
        /// <param name="hitMap">The hit map.</param>
        /// <param name="currentName">Current hit name.</param>
        protected virtual void RenderElementForHitTest(OpenGL gl, SceneElement sceneElement,
            Dictionary<uint, SceneElement> hitMap, ref uint currentName)
        {
            //  If the element is disabled, we're done.
            if (sceneElement.Enabled == false)
                return;

            //  Push each effect.
            foreach (var effect in sceneElement.Effects)
                if (effect.Enable)
                    effect.Push(gl, sceneElement);

            //  If the element has an object space, transform into it.
            if (sceneElement is IHasObjectSpace)
                ((IHasObjectSpace)sceneElement).PushObjectSpace(gl);

            //  If the element is volume bound, render the volume.
            if (sceneElement is IVolumeBound)
            {
                //  Load and map the name.
                gl.LoadName(currentName);
                hitMap[currentName] = sceneElement;

                //  Render the bounding volume.
                ((IVolumeBound)sceneElement).BoundingVolume.Render(gl, RenderMode.HitTest);

                //  Increment the name.
                currentName++;
            }

            //  Recurse through the children.
            foreach (var childElement in sceneElement.Children)
                RenderElementForHitTest(gl, childElement, hitMap, ref currentName);

            //  If the element has an object space, transform out of it.
            if (sceneElement is IHasObjectSpace)
                ((IHasObjectSpace)sceneElement).PopObjectSpace(gl);

            //  Pop each effect.
            for (int i = sceneElement.Effects.Count - 1; i >= 0; i--)
                if (sceneElement.Effects[i].Enable)
                    sceneElement.Effects[i].Pop(gl, sceneElement);
        }

        /// <summary>
        /// Renders the element.
        /// </summary>
        /// <param name="gl">The OpenGL context.</param>
        /// <param name="sceneElement">The scene element.</param>
        /// <param name="renderMode">The render mode.</param>
        protected virtual void RenderElement(OpenGL gl, SceneElement sceneElement, RenderMode renderMode, bool renderBoundingVolumes)
        {
            //  If the element is disabled, we're done.
            if (sceneElement.Enabled == false)
                return;

            //  Push each effect.
            foreach (var effect in sceneElement.Effects)
                if (effect.Enable)
                    effect.Push(gl, sceneElement);

            //  If the element has an object space, transform into it.
            if (sceneElement is IHasObjectSpace)
                ((IHasObjectSpace)sceneElement).PushObjectSpace(gl);

            //  If the element can be bound, bind it.
            if (sceneElement is IBindable)
                ((IBindable)sceneElement).Bind(gl);

            //  If the element has a material, push it.
            if (sceneElement is IHasMaterial && ((IHasMaterial)sceneElement).Material != null)
                ((IHasMaterial)sceneElement).Material.Push(gl);

            //  If the element can be rendered, render it.
            if (sceneElement is IRenderable)
                ((IRenderable)sceneElement).Render(gl, renderMode);

            //  If the element has a material, pop it.
            if (sceneElement is IHasMaterial && ((IHasMaterial)sceneElement).Material != null)
                ((IHasMaterial)sceneElement).Material.Pop(gl);

            //  If the element is volume bound and we are rendering volumes, render the volume.
            if (renderBoundingVolumes && sceneElement is IVolumeBound)
                ((IVolumeBound)sceneElement).BoundingVolume.Render(gl, renderMode);

            //  Recurse through the children.
            foreach (var childElement in sceneElement.Children)
                RenderElement(gl, childElement, renderMode, renderBoundingVolumes);

            //  If the element has an object space, transform out of it.
            if (sceneElement is IHasObjectSpace)
                ((IHasObjectSpace)sceneElement).PopObjectSpace(gl);

            //  Pop each effect.
            for (int i = sceneElement.Effects.Count - 1; i >= 0; i--)
            {
                if (sceneElement.Effects[i].Enable)
                    sceneElement.Effects[i].Pop(gl, sceneElement);
            }
        }

        #endregion Protected Methods

        #region Public Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="Scene"/> class.
        /// </summary>
		public Scene()
        {
            currentOpenGLContext = defaultCurrentOpenGLContext;
            sceneContainer = new SceneContainer();
            assets = new ObservableCollection<Asset>();
            clearColour = defaultClearColour;
            camera = defaultCamera;
            autoScaleToUnitVolume = defaultAutoScaleToUnitVolume;
            transformationToUnitVolume = new LinearTransformation();

            camera.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged);
            sceneContainer.SceneTreeChanged += new SceneTreeChangeEventHandler(OnSceneTreeChanged);
            sceneContainer.ContextChanged += new ContextChangeEventHandler(OnContextChanged);
            sceneContainer.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged);
        }

        #endregion Public Constructors

        #region Public Methods

        /// <summary>
        /// Performs a hit test on the scene. All elements that implement IVolumeBound will
        /// be hit tested.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns>The elements hit.</returns>
		public virtual IEnumerable<SceneElement> DoHitTest(int x, int y)
		{
            //  Create a result set.
            List<SceneElement> resultSet = new List<SceneElement>();

            //  Create a hitmap.
            Dictionary<uint, SceneElement> hitMap = new Dictionary<uint, SceneElement>();

            //	Create an array that will be the viewport.
			int[] viewport = new int[4];
			
			//	Get the viewport, then convert the mouse point to an opengl point.
			currentOpenGLContext.GetInteger(OpenGL.GL_VIEWPORT, viewport);
			y = viewport[3] - y;

			//	Create a select buffer.
			uint[] selectBuffer = new uint[512];
            currentOpenGLContext.SelectBuffer(selectBuffer.Length, selectBuffer);
			
			//	Enter select mode.
			currentOpenGLContext.RenderMode(OpenGL.GL_SELECT);
                        
			//	Initialise the names, and add the first name.
			currentOpenGLContext.InitNames();
			currentOpenGLContext.PushName(0);
		
			//	Push matrix, set up projection, then load matrix.
            currentOpenGLContext.MatrixMode(OpenGL.GL_PROJECTION);
			currentOpenGLContext.PushMatrix();
			currentOpenGLContext.LoadIdentity();
			currentOpenGLContext.PickMatrix(x, y, 4, 4, viewport);
			camera.TransformProjectionMatrix(currentOpenGLContext);
            
            currentOpenGLContext.MatrixMode(OpenGL.GL_MODELVIEW);
			currentOpenGLContext.LoadIdentity();

            //  Create the name.
            uint currentName = 1;

            //  Render the root for hit testing.
            RenderElementForHitTest(currentOpenGLContext, SceneContainer, hitMap, ref currentName);
													
			//	Pop matrix and flush commands.
            currentOpenGLContext.MatrixMode(OpenGL.GL_PROJECTION);
			currentOpenGLContext.PopMatrix();
            currentOpenGLContext.MatrixMode(OpenGL.GL_MODELVIEW);
			
            currentOpenGLContext.Flush();

			//	End selection.
            int hits = currentOpenGLContext.RenderMode(OpenGL.GL_RENDER);
			uint posinarray = 0;

            //  Go through each name.
            for (int hit = 0; hit < hits; hit++)
            {
                uint nameCount = selectBuffer[posinarray++];
                uint z1 = selectBuffer[posinarray++];
                uint z2 = selectBuffer[posinarray++];

                if (nameCount == 0)
                    continue;

                //	Add each hit element to the result set to the array.
                for (int name = 0; name < nameCount; name++)
                {
                    uint hitName = selectBuffer[posinarray++];
                    resultSet.Add(hitMap[hitName]);
                }
            }

            //  Return the result set.
			return resultSet;
		}

		/// <summary>
		/// This function draws all of the objects in the scene (i.e. every quadric
		/// in the quadrics arraylist etc).
		/// </summary>
        public virtual void Render(bool renderBoundingVolumes = false)
		{
            if (OpenGL.IsValidContext(currentOpenGLContext))
            {
                // Set the clear color.
                currentOpenGLContext.ClearColor(clearColour);

                // Clear buffers.
                currentOpenGLContext.Clear(OpenGL.GL_COLOR_BUFFER_BIT |
                                           OpenGL.GL_DEPTH_BUFFER_BIT |
                                           OpenGL.GL_STENCIL_BUFFER_BIT);

                // Reproject.
                currentOpenGLContext.MatrixMode(Enumerations.MatrixMode.Projection);
                currentOpenGLContext.LoadIdentity();
                camera.Project(currentOpenGLContext);

                // Setup modelview matrix.
                currentOpenGLContext.MatrixMode(Enumerations.MatrixMode.Modelview);
                currentOpenGLContext.LoadIdentity();

                // Transform if autoScaleToUnitVolume flag is set.
                if (autoScaleToUnitVolume)
                    transformationToUnitVolume.Transform(currentOpenGLContext);

                // Render the root element, this will then render the whole
                // of the scene tree.
                RenderElement(currentOpenGLContext, sceneContainer, RenderMode.Design, renderBoundingVolumes);

                currentOpenGLContext.Flush();
            }
		}

        /// <summary>
		/// Use this function to resize the scene window, and also to look through
		/// the current camera.
		/// </summary>
		/// <param name="width">Width of the screen.</param>
		/// <param name="height">Height of the screen.</param>
		public virtual void Resize(int width, int height)
		{
			if(OpenGL.IsValidContext(currentOpenGLContext) && width != -1 && height != -1)
			{
                // Resize.
				currentOpenGLContext.Viewport(0, 0, width, height);

                // Load identity projection matrix.
                currentOpenGLContext.MatrixMode(Enumerations.MatrixMode.Projection);
                currentOpenGLContext.LoadIdentity();
                // Set aspect ratio.
                if (height != 0)
                    camera.AspectRatio = (float)width / (float)height;
                else
                    camera.AspectRatio = 1.0f;
				// Then project.
                camera.Project(currentOpenGLContext);

                // Setup modelview matrix.
                currentOpenGLContext.MatrixMode(Enumerations.MatrixMode.Modelview);
                currentOpenGLContext.LoadIdentity();
			}
		}

        /// <summary>
        /// Create in the context of the supplied OpenGL instance.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        public virtual void CreateInContext(OpenGL gl)
        {
            if (!OpenGL.Equals(gl, currentOpenGLContext))
            {
                if (OpenGL.IsValidContext(currentOpenGLContext))
                    throw new Exception("Can't create scene in context '" +
                                        (OpenGL.IsValidContext(gl) ? gl.Renderer : "Null") +
                                        "' - it is already created in '" +
                                        (OpenGL.IsValidContext(currentOpenGLContext) ? currentOpenGLContext.Renderer : "Null") +
                                        "' OpenGL context.");


                if (OpenGL.IsValidContext(gl))
                {
                    currentOpenGLContext = gl;

                    CreateInContext(gl, sceneContainer);
                }
            }
        }

        /// <summary>
        /// Destroy in the context of the supplied OpenGL instance.
        /// </summary>
        public virtual void DestroyInContext()
        {
            if (OpenGL.IsValidContext(currentOpenGLContext))
            {
                DestroyInContext(currentOpenGLContext, sceneContainer);

                currentOpenGLContext = null;
            }
        }

        #endregion Public Methods

        #region Public Events

        [Description("Called when scene tree has been changed."), Category("SharpGL")]
        public event SceneTreeChangeEventHandler SceneTreeChanged;

        [Description("Called when OpenGL context has been changed."), Category("SharpGL")]
        public event ContextChangeEventHandler ContextChanged;

        [Description("Called when properties has been changed."), Category("SharpGL")]
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion Public Events
        
        #region Public Properties

        /// <summary>
        /// Gets the current OpenGL that the object exists in context.
        /// </summary>
        [Browsable(false)]
        public OpenGL CurrentOpenGLContext
        {
            get { return currentOpenGLContext; }
            private set { currentOpenGLContext = value; }
        }
        
        /// <summary>
        /// Gets or sets the scene container.
        /// </summary>
        /// <value>
        /// The scene container.
        /// </value>
        [Description("The top-level object in the Scene Tree"), Category("Scene")]
        public SceneContainer SceneContainer
        {
            get { return sceneContainer; }
            private set { sceneContainer = value; }
        }

        /// <summary>
        /// Gets the assets.
        /// </summary>
        [Description("The scene assets."), Category("Scene")]
        public ObservableCollection<Asset> Assets
        {
            get { return assets; }
            private set { assets = value; }
        }

        /// <summary>
        /// Gets or sets the color of the clear.
        /// </summary>
        /// <value>
        /// The color of the clear.
        /// </value>
        [Description("The background colour."), Category("Scene")]
        public GLColor ClearColor
        {
            get { return clearColour; }
            set
            {
                if (clearColour != value)
                {
                    clearColour = value;

                    NotifyPropertyChanged("ClearColor");
                }
            }
        }

        /// <summary>
        /// Gets or sets the current camera.
        /// </summary>
        /// <value>
        /// The current camera.
        /// </value>
        [Description("The current camera being used to view the scene."), Category("Scene")]
        public Camera Camera
        {
            get { return camera; }
            set
            {
                if (camera != value)
                {
                    camera.PropertyChanged -= new PropertyChangedEventHandler(OnPropertyChanged);

                    double oldCameraAspectRatio = camera.AspectRatio;

                    if (value != null)
                        camera = value;
                    else
                        camera = defaultCamera;

                    camera.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged);

                    camera.AspectRatio = oldCameraAspectRatio;

                    NotifyPropertyChanged("Camera");
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether
        /// auto scale to unit volume is enabled.
        /// </summary>
        /// <value>
        /// A value indicating whether auto scale to unit volume is enabled.
        /// </value>
        [Description("If true, the scene is scaling to unit volume."), Category("Scene")]
        public bool AutoScaleToUnitVolume
        {
            get { return autoScaleToUnitVolume; }
            set
            {
                if (autoScaleToUnitVolume != value)
                {
                    autoScaleToUnitVolume = value;

                    if (autoScaleToUnitVolume)
                        InitializeTransfomationToUnitVolume(transformationToUnitVolume, sceneContainer.ChildrenBoundingVolume);
                    else
                        transformationToUnitVolume.Reset();

                    NotifyPropertyChanged("AutoSizeToUnitVolume");
                }
            }
        }

        #endregion Public Properties
    }
}